코드 리뷰
JDBC 라이브러리 구현하기 미션 1, 2단계(1)
-
sql 구문의 파라미터를 셋팅할 때, 자료형이 명시되어 있지 않은 경우 setString(), setLong() 대신 setObject()를 사용할 수도 있다.
private void setSQLParameter(final Object parameter, final int parameterIndex, final PreparedStatement pstmt) throws SQLException { if (parameter instanceof String) { pstmt.setString(parameterIndex, (String) parameter); return; } if (parameter instanceof Long) { pstmt.setLong(parameterIndex, (Long) parameter); return; } ... if (parameter instanceof LocalDateTime) { pstmt.setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) parameter)); } }
private void setSQLParameter(final Object parameter, final int parameterIndex, final PreparedStatement pstmt) throws SQLException { pstmt.setObject(parameterIndex, parameter); }
-
SQLException으로 인한 예외가 발생했을 때, RuntimeException보다 다른 예외를 사용하는 것이 좋아 보인다.(DataAccessException으로 변경)
public void update(final String sql, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = conn.prepareStatement(sql)) { log.debug("query : {}", sql); setSqlParameter(obj, pstmt); pstmt.execute(); } catch (SQLException e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } }
-
JDBC API에서 제공하는 Connection, PreparedStatement, ResultSet 모두 AutoCloseable의 구현체이기 때문에 try-with-resources를 적용할 수 있다.
-
아래와 같은 방법으로 ResultSet 객체의 요소를 반환하도록 하면, DB에 같은 쿼리를 날려도 다른 결과가 나오는 경우가 발생할 수 있다.
때문에 ResultSet의 요소가 1개만 나오도록 제약을 걸어두어야 한다.
public <T> Optional<T> queryForObject(final String sql, final RowMapper<T> rowMapper, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = conn.prepareStatement(sql)) { ... ResultSet rs = pstmt.executeQuery(); if (rs.next()) { return Optional.of(rowMapper.mapRow(rs, rs.getRow())); } return Optional.empty(); } catch (SQLException e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } }
-
함수형 인터페이스를 정의할 때, @FunctionalInterface 어노테이션을 사용하면 추후 인터페이스에 다른 메서드가 추가되는 일을 미연에 방지할 수 있다.
@FunctionalInterface public interface RowMapper<T> { T mapRow(ResultSet rs, int rowNum) throws SQLException; }
JDBC 라이브러리 구현하기 미션 1, 2단계(2)
-
getPreparedStatement()의 경우 특별한 로직 없이 단순히 PreparedStatement를 반환하는 메서드이니 Util성 클래스로 분리하는 것이 좋아보인다.
- 리뷰어의 견해 : 메인 로직과 밀접하지 않고 변경될 가능성이 적은 기능의 경우 Util 클래스로 분리해도 괜찮다.
public <T> Optional<T> queryForObject(final String sql, final RowMapper<T> rowMapper, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn); final ResultSet rs = pstmt.executeQuery()) { log.debug("query : {}", sql); final List<T> result = new ArrayList<>(); while (rs.next()) { result.add(rowMapper.mapRow(rs, rs.getRow())); } validateResultSetSize(result); return Optional.of(result.get(0)); } catch (SQLException e) { log.error(e.getMessage(), e); throw new DataAccessException(e); } } private PreparedStatement getPreparedStatement(final String sql, final Object[] obj, final Connection conn) throws SQLException { final PreparedStatement pstmt = conn.prepareStatement(sql); setSqlParameter(obj, pstmt); return pstmt; }
-
‘결과가 2개 이상이라 어느 것을 반환해야 할지 알 수 없는’ 상황은 예외를 던지는 것 외에 사용자에게 해당 사실을 알릴 방법이 없지만, ‘조회하려는 레코드가 존재하지 않’는 상황은 예외를 던지지 않고도 null이나 Optional.empty()을 반환함으로서 사용자에게 알릴 수 있다.
private <T> void validateResultSetSize(List<T> result) { if (result.isEmpty()) { throw new DataAccessException("조회하려는 레코드가 존재하지 않습니다."); } if (result.size() > VALID_RESULT_COUNT) { throw new DataAccessException("조회하려는 레코드는 2개 이상일 수 없습니다."); } }
-
반복되는 try-with-resources 구문을 중복제거 해보는 건 어떤지?
다음과 같은 함수형 인터페이스를 정의하고,
public interface StatementExecutor<T> { T execute(final ResultSet rs) throws SQLException; }
try-catch문을 메서드로 분리해주면 try-catch문에 대한 중복을 제거할 수 있다.
public <T> List<T> tryCatchTemplate(final StatementExecutor<List<T>> executor, final String sql, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn); final ResultSet rs = pstmt.executeQuery()) { return executor.execute(rs); } catch (final SQLException exception) { log.error(exception.getMessage(), exception); throw new DataAccessException(exception); } }
커스텀 함수형 인터페이스의 메서드 시그니쳐에 SQLException를 표기해주면, ResultSet 객체에 대한 로직을 수행하고 있는 메서드
convertResultSetToInstances()
에 대해 예외 처리를 해주지 않아도 된다.public <T> List<T> query(final String sql, final RowMapper<T> rowMapper, final Object... obj) { return tryCatchTemplate(rs -> { log.debug("query : {}", sql); return convertResultSetToInstances(rowMapper, rs); }, sql, obj); }
JDBC 라이브러리 구현하기 3단계(1)
-
아래와 같이 코드를 작성한 경우, try 구문에 정의된 구문이 다른 update()에는 적용할 수 없다. update, query, queryForObject에 범용적으로 사용할 수 있게 만들 수는 없을까?
public <T> List<T> tryCatchTemplate(final StatementExecutor<List<T>> executor, final String sql, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn); final ResultSet rs = pstmt.executeQuery()) { return executor.execute(rs); } catch (final SQLException exception) { log.error(exception.getMessage(), exception); throw new DataAccessException(exception); } }
-
Connection을 주입받는 메서드가 필요해짐에 따라 update() 메서드를 오버로딩해주었는데, queryForObject()와 query()까지 오버로딩해줄 것인지?
오버로딩 없이 할 수 있는 방법은 없을까?
public void update(final String sql, final Object... obj) { try (final Connection conn = dataSource.getConnection(); final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn)) { log.debug("query : {}", sql); pstmt.execute(); } catch (SQLException exception) { log.error(exception.getMessage(), exception); throw new DataAccessException(exception); } } public void update(final Connection conn, final String sql, final Object... obj) { try (final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn)) { log.debug("query : {}", sql); pstmt.execute(); } catch (SQLException exception) { log.error(exception.getMessage(), exception); throw new DataAccessException(exception); } }
Connection을 static 객체로 다룰 수 있게 함으로써 JdbcTemplate 메서드에 직접 Connection 객체를 파라미터로 넘기지 않아도 되게끔 수정하였다.
private <T> T tryCatchTemplate(final StatementExecutor<T> executor, final String sql, final Object... obj) { final Connection conn = DataSourceUtils.getConnection(dataSource); try (final PreparedStatement pstmt = getPreparedStatement(sql, obj, conn)) { log.debug("query : {}", sql); return executor.execute(pstmt); } catch (SQLException exception) { log.error(exception.getMessage(), exception); throw new DataAccessException(exception); } }
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { Connection connection = TransactionSynchronizationManager.getResource(dataSource); if (connection != null) { return connection; } try { connection = dataSource.getConnection(); TransactionSynchronizationManager.bindResource(dataSource, connection); return connection; } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); } }